﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using dAmn;
using System.Net;
using System.Threading;
using llAma.Backend.Interface;
using System.Runtime.CompilerServices;
using llAma;
using System.IO;
using System.IO.Compression;

namespace llamaServer
{

    public partial class Connection
    {
        TcpClient mSocket;
        Guid id;
        public override int GetHashCode()
        {
            return id.GetHashCode();
        }
        public override bool Equals(object obj)
        {
            if (obj is Connection)
                return this.UUID == ((Connection)obj).UUID;
            else
                return false;
        }
        public string UUID
        {
            get { return id.ToString(); }
        }
        public IPEndPoint RemoteEndPoint
        {
            get { return (IPEndPoint)mSocket.Client.RemoteEndPoint; }
        }
        public IPEndPoint LocalEndPoint
        {
            get { return (IPEndPoint)mSocket.Client.LocalEndPoint; }
        }
        PacketProcessor dispatch;
        byte[] buffer = new byte[1024 * 16];
        Timer t;
        Timer to;
        ConnectionState mState = ConnectionState.STATE_WAITINGFORHANDSHAKE;
        public ConnectionState State
        {
            get { return mState; }
            set
            {
                //We don't want to trigger OnStateChange if state doesn't change
                if (mState == value)
                    return;
                OnStateChange(mState, value);
                mStateValidPacketsCache.Clear();
                mState = value;
            }
        }
        protected string[] stateValidHandshake = { "dAmnClient", "chatclient" };
        protected string[] stateValidAuth = { "login" };
        protected string[] stateValidMain = { "send", "pong", "join", "part" };
        protected string[] stateValidDisconnect = new string[0];
        List<string> mStateValidPacketsCache = new List<string>();
        protected bool IsValidForState(Packet p, string[] validPackets)
        {
            if (mStateValidPacketsCache.Contains(p.cmd))
                return true;
            foreach (string s in validPackets)
            {
                if (s == p.cmd)
                {
                    mStateValidPacketsCache.Add(s);
                    return true;
                }
            }
            return false;
        }
        protected bool IsValidForState(Packet p)
        {
            switch (mState)
            {
                case ConnectionState.STATE_DISCONNECTING:
                case ConnectionState.STATE_DISCONNECTED:
                    return false;
                case ConnectionState.STATE_WAITINGFORHANDSHAKE:
                    return IsValidForState(p, stateValidHandshake);
                case ConnectionState.STATE_WAITINGFORAUTH:
                    return IsValidForState(p, stateValidAuth);
                case ConnectionState.STATE_LOGGEDIN:
                    return IsValidForMainState(p);
                default:
                    return false;
            }
        }

        private bool IsValidForMainState(Packet p)
        {
            //Main state is valid for everything that isn't in Handshake or Auth states
            return !(IsValidForState(p, stateValidHandshake) || IsValidForState(p, stateValidAuth));
        }
        protected virtual void OnStateChange(ConnectionState oldState, ConnectionState newState)
        {
            //Do nothing here.
        }
        public bool Connected
        {

            get
            {
                if (mSocket == null || mSocket.Client == null) return false;
                return mSocket.Client.Connected;
            }

        }
        public Connection(TcpClient conn)
        {
            mSocket = conn;
            conn.NoDelay = true;
            id = Guid.NewGuid();
            mainLoop = DelayedExecution.GetPump();
            dispatch = new PacketProcessor(this);
            t = new Timer(TimerCB, this, 30000, Timeout.Infinite);
            to = new Timer(TimerDCCB, this, Timeout.Infinite, Timeout.Infinite);
            
            //Store these to avoid null reference exceptions later.
            mLocalEP = (IPEndPoint)conn.Client.LocalEndPoint;
            mRemoteEP = (IPEndPoint)conn.Client.RemoteEndPoint;
            outStream = inStream = conn.GetStream();
            inStream.BeginRead(buffer, 0, 1024 * 16, new AsyncCallback(recv), this);
            //conn.Client.BeginReceive(buffer, 0, 1024 * 16, SocketFlags.None, new AsyncCallback(recv), this);
        }
        bool gzipped = false;
        public bool CompressionEnabled
        {
            get
            {
                return gzipped;
            }
            set
            {
                if (gzipped == false)
                {
                    upgrading = true;
                    outStream = new DeflateStream(outStream, CompressionMode.Compress, true);
                    inStream = new DeflateStream(inStream, CompressionMode.Decompress, true);
                    inStream.BeginRead(buffer, 0, 1024 * 16, cmpRecv, this);
                    gzipped = true;
                }
                else if (gzipped == value) return;
                else
                {
                    throw new InvalidOperationException("A connection cannot be downgraded after enabling compression, The connection must be restarted by the client.");
                }
            }
        }
        Encoding e = Encoding.UTF8;
        private static void TimerDCCB(object state)
        {
            Connection c = (Connection)state;
            c.Disconnect("timed out");
            c.t.Change(Timeout.Infinite, Timeout.Infinite);
            c.to.Change(Timeout.Infinite, Timeout.Infinite);
        }
        private static void TimerCB(object state)
        {
            Connection c = (Connection)state;
            c.send("ping\n\0");
            c.to.Change((long)(new TimeSpan(0, 45, 0)).TotalMilliseconds, Timeout.Infinite);
        }
        private Stream inStream = null;
        private Stream outStream = null;
        private void send(string data)
        {
            byte[] outdata = e.GetBytes(data);
            try
            {
                if (mSocket != null && mSocket.Client.Connected)
                    outStream.BeginWrite(outdata, 0, outdata.Length, endWrite, this);
                else
                    if (!dcSent)
                        Disconnect("connection closed");
            }
            catch
            {
                mSocket = null;
                inStream = outStream = null;
                if (!dcSent)
                    Disconnect("send error");
            }
        }
        private void endWrite(IAsyncResult ar)
        {
            try
            {
                if (mSocket != null && mSocket.Client != null && mSocket.Client.Connected)
                    outStream.EndWrite(ar);
                else
                    Disconnect("connection closed");
            }
            catch
            {
                Disconnect("send error");
            }
        }
        static DEPool appPool = new DEPool();
        DelayedExecution dePump = appPool.GetPump();
        public bool InvokeRequired
        {
            get { return dePump.InvokeRequired; }
        }
        public void Invoke(Delegate d)
        {
            dePump.Invoke(d);
        }
        public void Invoke(Delegate d, params object[] args)
        {
            dePump.Invoke(d, args);
        }
        bool upgrading = false;
        private void cmpRecv(IAsyncResult ar)
        {
            try
            {
                if (mSocket != null && mSocket.Client != null && mSocket.Client.Connected)
                {
                    int bytes = inStream.EndRead(ar);
                    if (bytes < 1)
                    {
                        Disconnect("Connection Closed");
                        return;
                    }
                    incBuildString(buffer, 0, bytes);
                    if (Connected)
                    {
                        inStream.BeginRead(buffer, 0, 1024 * 16, new AsyncCallback(recv), this);
                    }
                }
                else
                {
                    return;
                }
            }
            catch
            {
                Disconnect("Bad data");
            }
        }
        private void recv(IAsyncResult ar)
        {
            try
            {
                if (upgrading) return;
                if (mSocket != null && mSocket.Client != null && mSocket.Client.Connected)
                {
                    int bytes = inStream.EndRead(ar);
                    if (bytes < 1)
                    {
                        Disconnect("Connection Closed");
                        return;
                    }
                    incBuildString(buffer, 0, bytes);
                    if (Connected)
                    {
                        inStream.BeginRead(buffer, 0, 1024 * 16, new AsyncCallback(recv), this);
                    }
                }
                else
                {
                    return;
                }
            }
            catch
            {
                Disconnect("Bad data");
            }
        }
        DelayedExecution mainLoop;
        delegate void OPDlg(string data);
        DateTime lastPacketIn = DateTime.Now;
        long avgTimeBetweenPackets = 30000;
        const long lowLimit = 4000; //1 packet per second is the max.
        long weight = 20;
        //This is a security check, And will return true if packets are being sent too fast.
        private bool UpdateAndCheckRMA()
        {
            TimeSpan ts = DateTime.Now - lastPacketIn;
            lastPacketIn = DateTime.Now;
            avgTimeBetweenPackets = (((long)ts.TotalMilliseconds) + (avgTimeBetweenPackets * weight)) / (weight + 1);
            if (avgTimeBetweenPackets < lowLimit)
            {
                return true;
            }
            return false;
        }
        protected void OnPacket(string data)
        {
            if (UpdateAndCheckRMA())
            {
                //We need to add a check here, to ignore login, pong and get. as well as the handshake.
                //This should be checked before introducing latency, just in case.
                if (avgTimeBetweenPackets <= (lowLimit / 8))
                {
                    Disconnect("packet limit reached");
                    return;
                }
                //Throttle the connection down.
                Thread.Sleep(new TimeSpan(0, 0, 0, 0, (int)((lowLimit - avgTimeBetweenPackets) * 2)));
            }

            /*if (mainLoop.InvokeRequired)
            {
                mainLoop.Invoke(new OPDlg(OnPacket), data);
                return;
            }*/

            if (!Connected)
            {
                //Don't know, don't care.
                return;
            }
            Packet p;
            if (!Packet.TryParse(data, out p))
            {
                this.Disconnect("bad data");
                return;
            }
            t.Change((long)(new TimeSpan(0, 1, 0)).TotalMilliseconds, Timeout.Infinite);
            to.Change(Timeout.Infinite, Timeout.Infinite);
            //Block pongs from propigating to the other classes, they don't need this packet.
            if (p.cmd == "pong")
                return;
            dispatch.ExecuteMethodsOnCommand(p, this);
        }
        DateTime mLastNonIdlePacket = DateTime.MinValue;
        [PacketHandler("join")]
        [PacketHandler("part")]
        [PacketHandler("kick")]
        [PacketHandler("ban")]
        [PacketHandler("unban")]
        [PacketHandler("send")]
        [PacketHandler("promote")]
        [PacketHandler("demote")]
        [PacketHandler("create")]
        [PacketHandler("kill")]
        protected void UpdateIdleTimer(Packet p, IConnection sender)
        {
            mLastNonIdlePacket = DateTime.Now;
        }
        [PacketHandler("join")]
        [PacketHandler("part")]
        [PacketHandler("kick")]
        //[PacketHandler("property")] // More stupidity.
        [PacketHandler("ban")]
        [PacketHandler("unban")]
        [PacketHandler("promote")]
        [PacketHandler("demote")]
        [PacketHandler("admin")]
        [PacketHandler("send")] // I am a dumbass. Period.
        [PacketHandler("create")]
        [PacketHandler("get")]
        [PacketHandler("set")]
        protected void handleNSPackets(Packet p, IConnection sender)
        {
            if (mCurrentUser == null)
            {
                Disconnect("bad msg");
                return;
            }
                Backend.Implementation.HandlePacket(p, sender);
        }
        //This is not a good idea, and should be removed in the future.
        protected virtual void OnParse(Packet p)
        {

        }
        //public event EventHandler<ClientEventArgs> Disconnecting;
        private bool dcSent = false;
        object LockObj = new object();
        public void Disconnect(string p)
        {
            if (mainLoop.InvokeRequired)
            {
                Thread.MemoryBarrier();
                if (dcSent) return;
                lock (LockObj)
                {
                    //We check again here, because threads may have set it to true after we checked the first time.
                    if (dcSent) return;
                    else dcSent = true;
                }
                Thread.MemoryBarrier();
                mainLoop.Invoke(new OPDlg(Disconnect), p);
                return;
            }

            List<IChannel> rooms = new List<IChannel>();
            foreach (KeyValuePair<string, IChannel> kv in mChannels)
            {
                rooms.Add(kv.Value);
            }
            //Cleanup.
            while (rooms.Count > 0)
            {
                rooms[0].ExternalPart((IConnection)this, p);
                rooms.RemoveAt(0);
            }
            ClientEventArgs c = new ClientEventArgs(this, p);
            try
            {
                //This event will allow the server to drop this client from its tables, and remove
                //The client from any rooms, and broadcast the reason for departure.
                if (mOnDisconnect != null)
                    mOnDisconnect(this, c);
            }
            catch { }
            if (p == "quit") p = "ok";
            if (Connected) send(string.Format("disconnect\ne={0}\n\n\0", p));
            //and now we close the socket.

            try { if (Connected) mSocket.Close(); }
            catch { }
            return;
        }
        private StringBuilder sbText = new StringBuilder();
        private void incBuildString(byte[] data, int offset, int count)
        {
            int intIndex = 0;
            for (intIndex = offset; intIndex < (offset + count); intIndex++)
            {
                if (data[intIndex] == 0)
                {
                    try
                    {
                        //If we disconencted, discard the packet.
                        if (!dcSent)
                            OnPacket(sbText.ToString());
                    }
                    catch
                    {
                    }
                    //This was thought to be good, I leave it here as a note that this is a BAD idea.
                    //This will grow to be the size of the max packet sent. 100 connections * 8kb
                    //You can quickly see this getting out of control.
                    //Clear the string builder instead of creating a new one
                    //This will reduce garbage collections and unnessary
                    //memory allocations.
                    // DO NOT DO THIS: sbText.Remove(0, sbText.Length);
                    //avgPacketSize is not modified outside of this function, which is on its own thread. Do not lock here.
                        avgPacketSize = ((avgPacketSize * apweight) + sbText.Length) / (apweight + 1);
                        sbText = new StringBuilder(avgPacketSize);
                }
                else
                {
                    if (sbText.Length > 1024 * 10)
                    {
                        sbText = null;
                        Disconnect("msg too big");
                        return;
                    }
                    sbText.Append((char)data[intIndex]);
                }
            }
        }
        const int apweight = 300;
        int avgPacketSize = 20;
    }
}
